﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Yahoo.Yui.Compressor;

namespace Ordina.SharePoint.CollectAndCrop
{
    /// <summary>
    /// This webcontrol can combine and minify javascript and css files.
    /// Example properties:
    ///     Files="/Style Library/js/,/Style Library/module/module.js" (comma seperated list of files and folders)
    ///     Active="True" (set false for development mode)
    ///     CacheSeconds="300" (Don't check for updates the first CacheSeconds seconds)
    ///     Mode=".js" (.js[default] or .css)
    ///     Media="all" (for CSS files)
    ///     TargetPath="/Style Library/" (where the combined file is put)
    ///     CompressCss="true"
    ///     JSEngine="YUI" (jsmin, YUI or none)
    /// </summary>
    /// TODO: tracing/errorhandling/errorchecking
    [ToolboxData("<{0}:CollectAndCropControl runat=\"server\"></{0}:CollectAndCropControl>")]
    public class CollectAndCropControl : WebControl
    {
        #region properties

        /// <summary>
        /// commaseperated list of files and folders
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue("")]
        [Localizable(true)]
        public string Files
        {
            get
            {   String s = (String)ViewState["Files"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {   ViewState["Files"] = value;
            }
        }

        /// <summary>
        /// Path where the combined file is put
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue("/Style Library/")]
        [Localizable(true)]
        public string TargetPath
        {
            get
            {
                String s = (String)ViewState["TargetPath"];
                return ((s == null) ? "/Style Library/" : s);
            }

            set
            {
                ViewState["TargetPath"] = value;
            }
        }

        /// <summary>
        /// set false for development mode
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue(true)]
        [Localizable(true)]
        public bool Active
        {
            get { return (ViewState["Active"] == null ? true : (bool)ViewState["Active"]); }

            set { ViewState["Active"] = value; }
        }

        /// <summary>
        /// switch for css compression
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue(false)]
        [Localizable(true)]
        public bool CompressCss
        {
            get { return (ViewState["CompressCss"] == null ? false : (bool)ViewState["CompressCss"]); }

            set { ViewState["CompressCss"] = value; }
        }

        /// <summary>
        /// Don't check for updates the first CacheSeconds seconds
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue("300")]
        [Localizable(true)]
        public double CacheSeconds
        {
            get {
                double i = 300;
                try { i = Convert.ToDouble(ViewState["CacheSeconds"]); }
                catch { }
                return i;
            }
            set { ViewState["CacheSeconds"] = value.ToString(); }
        }

        /// <summary>
        /// Media property for css files
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue("all")]
        [Localizable(true)]
        public string Media
        {
            get
            {
                String s = (String)ViewState["Media"];
                return ((s == null) ? "all" : s);
            }

            set
            {
                ViewState["Media"] = value;
            }
        }

        /// <summary>
        /// .js or .css
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue(".js")]
        [Localizable(true)]
        public string Mode
        {
            get
            {
                String s = (String)ViewState["Mode"];
                return ((s == null) ? ".js" : s);
            }

            set
            {
                ViewState["Mode"] = value;
            }
        }

        /// <summary>
        /// Engine for js compression, jsmin, YUI or none
        /// </summary>
        [Bindable(true)]
        [Category("CollectAndCrop")]
        [DefaultValue("all")]
        [Localizable(true)]
        public string JSEngine
        {
            get
            {
                String s = (String)ViewState["JSEngine"];
                return ((s == null) ? "jsmin" : s);
            }

            set
            {
                ViewState["JSEngine"] = value;
            }
        }

        private const string CombinedFileName = "combined";
        private TraceSwitch traceSwitch = new TraceSwitch("Ordina.SharePoint.CollectAndCrop", "Trace switch for the Ordina.SharePoint.CollectAndCrop");
        private TraceHelper trace = new TraceHelper("Ordina.SharePoint.CollectAndCrop");

        #endregion

        /// <summary>
        /// The main function of this control
        /// </summary>
        /// <param name="writer"></param>
        protected override void Render(HtmlTextWriter writer)
        {
            trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "Render started");
            SPSecurity.RunWithElevatedPrivileges(delegate()
                {

                    if (!Active)
                    {
                        trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "development mode: collect and include every file");
                        List<SourceFile> sources = GetSources();
                        RenderIncludes(writer, sources);
                    }
                    else
                    {
                        if (HttpRuntime.Cache[CombinedFileName + ID + Mode] != null)
                        {
                            trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "inside cachetime: no rebuild just render");
                            RenderInclude(writer);
                        }
                        else
                        {
                            //find combined file
                            SPWeb web = SPContext.Current.Site.RootWeb;
                            SPFile file = web.GetFile(TargetPath + CombinedFileName + ID + Mode);
                            if (file.Exists)
                            {
                                //check if sourcefiles changed
                                List<SourceFile> sources = GetSources();
                                DateTime maxTimeLastModified = GetMaxTime(sources);

                                if (maxTimeLastModified > file.TimeLastModified)
                                {
                                    trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "sources changed: rebuild and render");
                                    BuildCombinedInclude(sources);
                                    RenderInclude(writer);
                                }
                                else
                                {
                                    trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "sources unchanged: just render");
                                    RenderInclude(writer);
                                }
                            }
                            else
                            {
                                trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "first run: build and render");
                                List<SourceFile> sources = GetSources();
                                BuildCombinedInclude(sources);
                                RenderInclude(writer);
                            }
                            //add entry in cache so that for Cacheseconds nothing is build or checked
                            CacheThis();
                        }
                    }
                });
        }

        #region include statement(s)
        /// <summary>
        /// Render the include statement for the combined file
        /// </summary>
        /// <param name="writer"></param>
        private void RenderInclude(HtmlTextWriter writer)
        {
            trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "RenderInclude");

            if (Mode==".js")
                writer.WriteLine("<script type=\"text/javascript\" src=\"{0}\"></script>", TargetPath + CombinedFileName + ID + Mode);
            else
                writer.WriteLine("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" media=\"{1}\" />", TargetPath + CombinedFileName + ID + Mode, Media);
        }

        /// <summary>
        /// Render the include statements for all individual files
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="sources"></param>
        private void RenderIncludes(HtmlTextWriter writer, List<SourceFile> sources)
        {
            foreach (SourceFile source in sources)
            {
                if (Mode==".js")
                {
                    writer.WriteLine("<script type=\"text/javascript\" src=\"{0}\"></script>", GetwebUrl(source.Path));
                }
                else
                {
                    writer.WriteLine("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" media=\"{1}\" />", GetwebUrl(source.Path), Media);
                }
            }
        }

        private string GetwebUrl(string thePath)
        {
            if (!thePath.Contains("\\12\\template\\layouts\\")) {
                return thePath;
            } else {
                string res = thePath.Replace("\\", "/");
                return "/_" + res.Substring(res.IndexOf("12/template/") + 12);
                //C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\template\layouts\yui-2.8\reset\reset-min.css
            }
        }



        #endregion

        /// <summary>
        /// Get a list of all sourcefiles
        /// </summary>
        /// <returns>list of sources</returns>
        private List<SourceFile> GetSources()
        {
            List<SourceFile> result = new List<SourceFile>();
            string root = (SPContext.Current.Site.RootWeb.ServerRelativeUrl == "/") ? "" : SPContext.Current.Site.RootWeb.ServerRelativeUrl;
            foreach (string path in Files.Split(','))
            {
                if (path.ToLower().StartsWith("/_layouts/"))
                {
                    string localpath = HttpContext.Current.Server.MapPath(path);
                    if (path.EndsWith("/"))
                    {
                        //complete folder form layouts
                        if (Directory.Exists(localpath))
                        {
                            result.AddRange(GetSources(localpath));
                        }
                    }
                    else
                    {
                        //file from layouts
                        if (File.Exists(localpath))
                        {
                            result.Add(new SourceFile(localpath, File.GetLastWriteTime(localpath), false));
                        }
                    }
                }
                else
                {
                    if (path.EndsWith("/"))
                    {
                        //folder from SP
                        SPFolder folder = SPContext.Current.Site.RootWeb.GetFolder(root + path);
                        if (folder.Exists)
                        {
                            result.AddRange(GetSources(folder));
                        }
                        else
                        {
                            trace.WriteLineIf(traceSwitch.TraceWarning, TraceLevel.Warning, "GetSources - Folder {0} not found", root + path);
                        }
                    }
                    else
                    {
                        //file form SP
                        SPFile file = SPContext.Current.Site.RootWeb.GetFile(root + path);
                        if (file.Exists)
                        {
                            if (file.Url.EndsWith(Mode))
                                result.Add(new SourceFile(file.ServerRelativeUrl, file.TimeLastModified, true));
                        }
                        else
                        {
                            trace.WriteLineIf(traceSwitch.TraceWarning, TraceLevel.Warning, "GetSources - File {0} not found", root + path);
                        }
                    }
                }
            }
            trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "GetSources - {0} files found", result.Count.ToString());
            return result;
        }

        private IEnumerable<SourceFile> GetSources(string localpath)
        {
            List<SourceFile> result = new List<SourceFile>();
            foreach (string filename in Directory.GetFiles(localpath))
            {
                if (filename.EndsWith(Mode))
                {
                    if (!filename.EndsWith(CombinedFileName + ID + Mode))
                    {
                        string filepath = Path.Combine(localpath, filename);
                        result.Add(new SourceFile(filepath, File.GetLastWriteTime(filepath), false));
                    }
                }
            }
            result.Sort(delegate(SourceFile s1, SourceFile s2) { return s1.Path.CompareTo(s2.Path); });
            return result;
        }
        
        /// <summary>
        /// Get all sourcefiles from a folder
        /// </summary>
        /// <param name="folder">the folder</param>
        /// <returns>sorted list of sourcefiles</returns>
        private IEnumerable<SourceFile> GetSources(SPFolder folder)
        {
            List<SourceFile> result = new List<SourceFile>();
            foreach (SPFile file in folder.Files)
            {
                if (file.Url.EndsWith(Mode))
                {
                    if (!file.Url.EndsWith("/" + CombinedFileName + ID + Mode))
                        result.Add(new SourceFile(file.ServerRelativeUrl, file.TimeLastModified, true));
                }
            }
            result.Sort(delegate(SourceFile s1, SourceFile s2) { return s1.Path.CompareTo(s2.Path); });
            return result;
        }

        /// <summary>
        /// Get the time of the last modified sourcefile
        /// </summary>
        /// <param name="sources"></param>
        /// <returns>DateTime lastmodified</returns>
        private DateTime GetMaxTime(List<SourceFile> sources)
        {
            if (sources.Count == 0)
                return DateTime.Now;
            else
            {
                DateTime result = DateTime.MinValue;
                foreach (SourceFile sf in sources)
                {
                    if (sf.LastModified > result)
                        result = sf.LastModified;
                }
                return result;
            }
        }

        /// <summary>
        /// Combine the content of all the sourcefiles. Minify the javascript files.
        /// </summary>
        /// <param name="sources"></param>
        private void BuildCombinedInclude(List<SourceFile> sources)
        {
            string root = (SPContext.Current.Site.RootWeb.ServerRelativeUrl == "/") ? "" : SPContext.Current.Site.RootWeb.ServerRelativeUrl;

            //read
            string combined = "/* combined */ \n"; //SharePoint does not like empty files
            foreach (SourceFile source in sources)
            {
                if (source.FromSharePoint)
                {
                    SPFile file = SPContext.Current.Site.RootWeb.GetFile(root + source.Path);
                    using (System.IO.StreamReader reader = new System.IO.StreamReader(file.OpenBinaryStream(SPOpenBinaryOptions.SkipVirusScan)))
                    {
                        combined = combined + reader.ReadToEnd() + "\n";
                    }
                }
                else {
                    using (System.IO.StreamReader reader = File.OpenText(source.Path))
                    {
                        combined = combined + reader.ReadToEnd() + "\n";
                    }
                }
            }

            if (Mode == ".js")
            {
                trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "BuildCombinedInclude - start minify js");
                if (JSEngine == "YUI")
                {
                    combined = JavaScriptCompressor.Compress(combined);
                }
                else if (JSEngine == "none")
                {
                    //do nothing
                }
                else
                {
                    JavaScriptMinifier jsm = new JavaScriptMinifier();
                    combined = jsm.Minify(combined);
                }
            }
            else if (CompressCss && Mode == ".css")
            {
                trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "BuildCombinedInclude - start minify css");
                combined = CssCompressor.Compress(combined, 120, CssCompressionType.StockYuiCompressor);
            }
            //save
            SaveCombined(combined);
        }

        /// <summary>
        /// Add a entry in the cache for this instance
        /// </summary>
        private void CacheThis()
        {
            //maybe use DateTime.UtcNow?
            trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "CacheThis {0} until {1}", CombinedFileName + ID + Mode, DateTime.Now.AddSeconds(CacheSeconds).ToShortTimeString());
            HttpRuntime.Cache.Add(CombinedFileName + ID + Mode, "cached", null, DateTime.Now.AddSeconds(CacheSeconds), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null);
        }

        /// <summary>
        /// Save the combined file
        /// </summary>
        /// <param name="content"></param>
        private void SaveCombined(string content)
        {
            byte[] binContent = System.Text.Encoding.UTF8.GetBytes(content);

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                SPWeb web = SPContext.Current.Site.RootWeb;
                bool oldAllowUnsafeUpdates = web.AllowUnsafeUpdates;
                web.AllowUnsafeUpdates = true;
                SPFile file = web.GetFile(TargetPath + CombinedFileName + ID + Mode);
                if (file.Exists)
                {
                    trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "SaveCombined - update the combined file");
                    file.CheckOut();
                    file.SaveBinary(binContent);
                    file.CheckIn("Saved by CollectAndCrop", SPCheckinType.MajorCheckIn);
                }
                else
                {
                    trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "SaveCombined - add the combined file to " + TargetPath);
                    SPFolder spfolder = web.GetFolder(TargetPath);

                    trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "SaveCombined - add file");
                    spfolder.Files.Add(CombinedFileName + ID + Mode, binContent, true);
                    
                    trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "SaveCombined - get file");
                    file = web.GetFile(TargetPath + CombinedFileName + ID + Mode);

                    trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "SaveCombined - checkin file");
                    file.CheckIn("Saved by CollectAndCrop", SPCheckinType.MajorCheckIn);
                }
                web.AllowUnsafeUpdates = oldAllowUnsafeUpdates;
            });
        }

        /// <summary>
        /// No tag needed
        /// </summary>
        /// <param name="writer">a HtmlTextWriter</param>
        public override void RenderBeginTag(HtmlTextWriter writer)
        {
            //base.RenderBeginTag(writer);
        }

        /// <summary>
        /// No tag needed
        /// </summary>
        /// <param name="writer">a HtmlTextWriter</param>
        public override void RenderEndTag(HtmlTextWriter writer)
        {
            //base.RenderEndTag(writer);
        }

    }

    class SourceFile
    {
        public string Path;
        public DateTime LastModified;
        public bool FromSharePoint;

        public SourceFile(string path, DateTime lastModified, bool fromSharePoint)
        {
            Path = path;
            LastModified = lastModified;
            FromSharePoint = fromSharePoint;
        }
    }
}
